/**
 * Copyright (C) 2009 Noel Ang
 *
 * This file is part of Samsara.
 *
 * Samsara is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Samsara is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Samsara.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/
package com.lunarshades.samsara.ui.actions;

import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ImageIcon;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import com.lunarshades.samsara.ui.Filterable;
import com.lunarshades.samsara.ui.RosterTableModel;

/**
 * Action responsible for filtering what rows are seen in a table.
 *
 * @author Noel Ang, sparta.variable@gmail.com
 * @see com.lunarshades.samsara.ui.Filterable
 */
public class RosterFilterAction extends AbstractAction
        implements ListSelectionListener, PropertyChangeListener {

public RosterFilterAction(JTable table) {
    super(FILTER_NAME);
    mTable = table;

    URL smallIconUrl = getClass().getResource(
            "/com/lunarshades/samsara/ui/icons/zoomIn16.gif"
    );
    putValue(Action.SMALL_ICON, new ImageIcon(smallIconUrl));
    putValue(Action.SHORT_DESCRIPTION, FILTER_TEXT);
    putValue(Action.MNEMONIC_KEY, KeyEvent.VK_F);
    setEnabled(!mTable.getSelectionModel().isSelectionEmpty());
    // Listen to selection changes to enable/disable the action as appropriate
    mTable.getSelectionModel().addListSelectionListener(this);
}

private Collection<Filterable> included() {
    int[] selectedRows = mTable.getSelectedRows();
    RosterTableModel model = (RosterTableModel) mTable.getModel();
    int rowCount = selectedRows.length;
    Set<Filterable> filterables = new HashSet<Filterable>(rowCount);
    for (int row = 0; row < rowCount; ++row) {
        filterables.add(model.getRowAt(mTable.convertRowIndexToModel(
                selectedRows[row]
        )
        )
        );
    }
    return filterables;
}

private void updateStatus(Collection<Filterable> filtereds) {
    updateActionStatus();
    synchronized (mFilterSets) {
        mFilterSets.push(filtereds);
    }
    if (!filtereds.isEmpty()) {
        firePropertyChange(FILTER_PROPERTY, null, null);
    }
}

private void updateActionStatus() {
    putValue(Action.NAME, actionName());
    setEnabled(!mTable.getSelectionModel().isSelectionEmpty());
}

protected String actionName() {
    return FILTER_NAME;
}

public Action getDefilterAction() {
    if (mDefilterAction == null) {
        mDefilterAction = new RosterDefilterAction();
        addPropertyChangeListener(mDefilterAction);
    }
    return mDefilterAction;
}

private void purgeFilters() {
    synchronized (mFilterSets) {
        while (!mFilterSets.isEmpty()) {
            mFilterSets.pop();
        }
        updateActionStatus();
        firePropertyChange(FILTER_PROPERTY, null, null);
    }
}

public void actionPerformed(ActionEvent e) {
    Collection<Filterable> included = included();
    Collection<Filterable> excluded = new ArrayList<Filterable>();
    if (!included.isEmpty()) {
        RosterTableModel model = (RosterTableModel) mTable.getModel();
        int rowCount = model.getRowCount();
        for (int modelRow = 0; modelRow < rowCount; ++modelRow) {
            Filterable filterable = model.getRowAt(modelRow);
            if (included.contains(filterable)) {
                filterable.setIsIncluded(true);
            } else {
                if (filterable.isIncluded()) {
                    filterable.setIsIncluded(false);
                    excluded.add(filterable);
                }
            }
        }
        updateStatus(excluded);
    }
}

public void valueChanged(ListSelectionEvent e) {
    updateActionStatus();
}

/**
 * This method gets called when a bound property is changed.
 *
 * @param evt A PropertyChangeEvent object describing the event source and the
 *            property that has changed.
 */
public void propertyChange(PropertyChangeEvent evt) {
    if (LoadDataAction.FILE_LOADED.equals(evt.getPropertyName())) {
        purgeFilters();
    }
}

private class RosterDefilterAction extends AbstractAction
        implements ListSelectionListener, PropertyChangeListener {

    private RosterDefilterAction() {
        super(DEFILTER_NAME);
        putValue(Action.MNEMONIC_KEY, KeyEvent.VK_T);
        putValue(Action.SHORT_DESCRIPTION, DEFILTER_TEXT);
        URL smallIconUrl = getClass().getResource(
                "/com/lunarshades/samsara/ui/icons/zoomOut16.gif"
        );
        putValue(Action.SMALL_ICON, new ImageIcon(smallIconUrl));
        setEnabled(false);
    }

    private Collection<Filterable> excluded() {
        synchronized (mFilterSets) {
            boolean noFilter = mFilterSets.isEmpty();
            Set<Filterable> filterables = new HashSet<Filterable>(
                    noFilter ? 0 : mFilterSets.peek().size()
            );
            if (!noFilter) {
                filterables.addAll(mFilterSets.peek());
            }
            return filterables;
        }
    }

    private void updateStatus(Collection<Filterable> filtereds) {
        synchronized (mFilterSets) {
            if (!mFilterSets.isEmpty()) {
                Collection<Filterable> currentExclusionSet = mFilterSets.peek();
                if (filtereds.size() == currentExclusionSet.size()
                        && filtereds.containsAll(currentExclusionSet)) {
                    mFilterSets.pop();
                    firePropertyChange(DEFILTER_PROPERTY, null, null);
                }
            }
            updateActionStatus();
        }
    }

    private void updateActionStatus() {
        synchronized (mFilterSets) {
            setEnabled(!mFilterSets.isEmpty());

            int filterSetCount = mFilterSets.size();
            StringBuffer buffer = new StringBuffer();
            buffer.append(DEFILTER_NAME);
            if (filterSetCount > 0) {
                buffer.append(" (").append(filterSetCount).append(")");
            }
            putValue(Action.NAME, buffer.toString());
        }
    }

    public void actionPerformed(ActionEvent e) {
        Collection<Filterable> excluded = excluded();
        if (!excluded.isEmpty()) {
            RosterTableModel model = (RosterTableModel) mTable.getModel();
            int rowCount = model.getRowCount();
            for (int modelRow = 0; modelRow < rowCount; ++modelRow) {
                Filterable filterable = model.getRowAt(modelRow);
                if (excluded.contains(filterable)) {
                    filterable.setIsIncluded(true);
                }
            }
        }
        // Update status even if filter set just processed was empty;
        // that filter set still needs to be popped and all interested
        // listeners notified.
        updateStatus(excluded);

        // Select all the rows in the table that were used to create the
        // filter that was just discarded.  In other words, highlight
        // all filterables that are flagged for inclusion BUT excluding
        // the ones newly added (from the excluded set).
        mTable.clearSelection();
        RosterTableModel model = (RosterTableModel) mTable.getModel();
        ListSelectionModel selectionModel = mTable.getSelectionModel();
        int rowCount = model.getRowCount();
        for (int modelRow = 0; modelRow < rowCount; ++modelRow) {
            int viewRow = mTable.convertRowIndexToView(modelRow);
            if (viewRow != -1) {
                if (!excluded.contains(model.getRowAt(modelRow))) {
                    assert model.getRowAt(modelRow).isIncluded();
                    selectionModel.addSelectionInterval(viewRow, viewRow);
                }
            }
        }
    }

    public void valueChanged(ListSelectionEvent e) {
        updateActionStatus();
    }

    public void propertyChange(PropertyChangeEvent evt) {
        if (FILTER_PROPERTY.equals(evt.getPropertyName())) {
            updateActionStatus();
        }
    }
}

private final static String FILTER_NAME = "Filter";
private final static String FILTER_TEXT = "Hide unselected table rows";
private final static String DEFILTER_NAME = "De-filter";
private final static String DEFILTER_TEXT =
        "Remove the most recently applied filter";
private final JTable mTable;
private RosterDefilterAction mDefilterAction;
private final Stack<Collection<Filterable>> mFilterSets =
        new Stack<Collection<Filterable>>();

public static final String FILTER_PROPERTY = "Filter";
public static final String DEFILTER_PROPERTY = "Defilter";
}
